feat(shared,ui): Render plan badge in OrganizationSwitcher#9149
feat(shared,ui): Render plan badge in OrganizationSwitcher#9149dstaley wants to merge 6 commits into
Conversation
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
🦋 Changeset detectedLatest commit: 1a38fc1 The changes in this PR will be included in the next version bump. This PR includes changesets to release 23 packages
Not sure what this means? Click here to learn what changesets are. Click here if you're a maintainer who wants to add another changeset to this PR |
| )} | ||
| </AcceptedInvitationsProvider> | ||
| </Flow.Root> | ||
| <SubscriberTypeContext.Provider value='organization'> |
There was a problem hiding this comment.
the only thing changed here is wrapping the existing Flow.Root in the SubscriberTypeContext.Provider
|
Note Reviews pausedIt looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the Use the following commands to manage reviews:
Use the checkboxes below for quick actions:
📝 WalkthroughWalkthrough
ChangesOrganization plan badge
Estimated code review effort: 3 (Moderate) | ~20 minutes Sequence Diagram(s)sequenceDiagram
participant OrganizationSwitcherTrigger
participant useSubscription
participant Billing
participant OrganizationPreview
participant Badge
OrganizationSwitcherTrigger->>useSubscription: Read active organization subscription
useSubscription->>Billing: Fetch subscription
Billing-->>useSubscription: Return subscription items
useSubscription-->>OrganizationSwitcherTrigger: Loading or plan item state
OrganizationSwitcherTrigger->>OrganizationPreview: Pass plan badge
OrganizationPreview->>Badge: Render loading or plan badge
Suggested reviewers: Poem
🚥 Pre-merge checks | ✅ 5✅ Passed checks (5 passed)
✨ Finishing Touches📝 Generate docstrings
Comment |
|
|
||
| if (isLoading) { | ||
| // 5ch is specifically chosen to balance the size of "Free" versus paid plans in Inter | ||
| return <LoadingBadge sx={{ width: '5ch' }} />; |
| return ( | ||
| <Badge | ||
| elementDescriptor={descriptors.organizationSwitcherTriggerBadge} | ||
| elementId={descriptors.organizationSwitcherTriggerBadge.setId(slug)} |
There was a problem hiding this comment.
added this so you can specifically target certain plans to customize the badge
@clerk/astro
@clerk/backend
@clerk/chrome-extension
@clerk/clerk-js
@clerk/electron
@clerk/electron-passkeys
@clerk/eslint-plugin
@clerk/expo
@clerk/expo-passkeys
@clerk/express
@clerk/fastify
@clerk/hono
@clerk/localizations
@clerk/nextjs
@clerk/nuxt
@clerk/react
@clerk/react-router
@clerk/shared
@clerk/tanstack-react-start
@clerk/testing
@clerk/ui
@clerk/upgrade
@clerk/vue
commit: |
API Changes Report
Summary
@clerk/sharedCurrent version: 4.25.2 Subpath
|
There was a problem hiding this comment.
Actionable comments posted: 2
🧹 Nitpick comments (1)
packages/ui/src/components/OrganizationSwitcher/__tests__/OrganizationSwitcher.test.tsx (1)
90-166: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick winMissing coverage for the free-plan (
primary) and loading-state paths.Both new tests only cover a paid plan (
isDefault: false→secondarycolorScheme). Per the PR objective, free plans should render with theprimarycolor scheme, and there's also an animated loading state (LoadingBadge) rendered while the subscription is being resolved — neither is exercised here. As per path instructions,**/*.{test,spec}.{ts,tsx,js,jsx}requires that "Unit tests are required for all new functionality" and to "Verify proper error handling and edge cases."Consider adding:
- A test with
isDefault: trueasserting theprimarycolorScheme is applied.- A test asserting the pulsing
LoadingBadgerenders whilegetSubscriptionis pending.Do you want me to draft these additional test cases?
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@packages/ui/src/components/OrganizationSwitcher/__tests__/OrganizationSwitcher.test.tsx` around lines 90 - 166, Add coverage in OrganizationSwitcher tests for both missing paths: configure the mocked subscription plan with isDefault: true and assert the rendered plan badge uses the primary color scheme, then keep getSubscription pending and assert the animated LoadingBadge appears during resolution. Reuse the existing fixtures and showPlanName setup while preserving the paid-plan and billing-disabled tests.Source: Path instructions
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In
`@packages/ui/src/components/OrganizationSwitcher/OrganizationSwitcherTrigger.tsx`:
- Line 66: Update OrganizationSwitcherTrigger’s badge rendering and the
corresponding PlanBadge usage around the noted lines so PlanBadge is mounted
only when showPlanName is enabled. Ensure useSubscription() is not invoked on
the default showPlanName={false} path, while preserving the existing badge
behavior when showPlanName is true.
- Around line 106-108: Update the LoadingBadge render in
OrganizationSwitcherTrigger to apply the same organizationSwitcherTriggerBadge
descriptor and corresponding elementId used by the normal plan badge, while
preserving its loading width and existing behavior.
---
Nitpick comments:
In
`@packages/ui/src/components/OrganizationSwitcher/__tests__/OrganizationSwitcher.test.tsx`:
- Around line 90-166: Add coverage in OrganizationSwitcher tests for both
missing paths: configure the mocked subscription plan with isDefault: true and
assert the rendered plan badge uses the primary color scheme, then keep
getSubscription pending and assert the animated LoadingBadge appears during
resolution. Reuse the existing fixtures and showPlanName setup while preserving
the paid-plan and billing-disabled tests.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Repository YAML (base), Repository UI (inherited)
Review profile: CHILL
Plan: Pro Plus
Run ID: beb9aa54-c754-49fd-a67d-36e1d6dce633
📒 Files selected for processing (9)
.changeset/hungry-lands-fold.mdpackages/shared/src/types/clerk.tspackages/ui/src/components/OrganizationSwitcher/OrganizationSwitcherTrigger.tsxpackages/ui/src/components/OrganizationSwitcher/__tests__/OrganizationSwitcher.test.tsxpackages/ui/src/components/OrganizationSwitcher/index.tsxpackages/ui/src/customizables/elementDescriptors.tspackages/ui/src/elements/Badge.tsxpackages/ui/src/internal/appearance.tspackages/ui/src/styledSystem/animations.ts
| if (isLoading) { | ||
| // 5ch is specifically chosen to balance the size of "Free" versus paid plans in Inter | ||
| return <LoadingBadge sx={{ width: '5ch' }} />; |
There was a problem hiding this comment.
🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win
Apply the OrganizationSwitcher badge descriptor to the loading state.
The normal plan badge uses organizationSwitcherTriggerBadge, but LoadingBadge is rendered without an elementDescriptor or elementId. Appearance overrides therefore cannot target the loading placeholder. As per path instructions, themable elements under packages/ui/src must apply element descriptors.
Suggested fix
- return <LoadingBadge sx={{ width: '5ch' }} />;
+ return (
+ <LoadingBadge
+ elementDescriptor={descriptors.organizationSwitcherTriggerBadge}
+ elementId={descriptors.organizationSwitcherTriggerBadge.setId('loading')}
+ sx={{ width: '5ch' }}
+ />
+ );📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| if (isLoading) { | |
| // 5ch is specifically chosen to balance the size of "Free" versus paid plans in Inter | |
| return <LoadingBadge sx={{ width: '5ch' }} />; | |
| if (isLoading) { | |
| // 5ch is specifically chosen to balance the size of "Free" versus paid plans in Inter | |
| return ( | |
| <LoadingBadge | |
| elementDescriptor={descriptors.organizationSwitcherTriggerBadge} | |
| elementId={descriptors.organizationSwitcherTriggerBadge.setId('loading')} | |
| sx={{ width: '5ch' }} | |
| /> | |
| ); |
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In
`@packages/ui/src/components/OrganizationSwitcher/OrganizationSwitcherTrigger.tsx`
around lines 106 - 108, Update the LoadingBadge render in
OrganizationSwitcherTrigger to apply the same organizationSwitcherTriggerBadge
descriptor and corresponding elementId used by the normal plan badge, while
preserving its loading width and existing behavior.
Source: Path instructions
alexcarpenter
left a comment
There was a problem hiding this comment.
can you add an example showing how this wraps?
| colorScheme={isDefault ? 'primary' : 'secondary'} | ||
| sx={{ whiteSpace: 'nowrap' }} | ||
| > | ||
| <Text |
There was a problem hiding this comment.
do we need the extra text usage in the badge vs supplying the sizing to the badge component?
There was a problem hiding this comment.
nah, we use <Text> in Badge in a few places, but here we don't really need it. Removed!
|
@alexcarpenter played around a bit and this is where I landed |
| {organization.name} {badge} | ||
| <Box | ||
| as='span' | ||
| sx={{ flex: '1 1 auto', minWidth: 0, overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }} |
There was a problem hiding this comment.
I wonder if the org name should include a longer min width to ensure it never is completely hidden with a long plan name. not sure if you tried that combo.
There was a problem hiding this comment.
The styling is somewhat confusing, but the general structure is that the parent element has max-width: 30ch, and the badge has max-width: 14ch, so the badge will only ever take up slightly less than half of the total space. The min-width rule here is to allow for a flex item to be narrower than its intrinsic width. From testing I don't think the min-width actually has any impact on the minimum width the organization name has to render within (which should always be 16ch).
ApprovabilityVerdict: Needs human review This PR introduces a new user-facing feature (plan badge display) with billing system integration. Additionally, there's an unresolved review comment about unnecessary subscription fetches when the feature is disabled, which affects runtime behavior. You can customize Macroscope's approvability policy. Learn more. |
There was a problem hiding this comment.
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (2)
packages/shared/src/types/clerk.ts (1)
2229-2234: 📐 Maintainability & Code Quality | 🟠 Major | 🏗️ Heavy liftJSDoc for
renderPlanBadgedoesn't document the custom function option.The JSDoc only describes the boolean behavior ("Shows the Clerk Billing Plan name") but the prop also accepts an async function for custom badge rendering. This is a public/reference-facing API that may render in generated Clerk Docs, so the JSDoc should accurately describe both modes, including the function's return shape (
{ label, slug, colorScheme? }).As per path instructions, JSDoc on public/reference-facing APIs is customer-facing documentation. The Docs team may need to review this change.
📝 Proposed JSDoc update
/** - * Shows the Clerk Billing Plan name for the currently active organization. + * Controls the plan badge displayed in the OrganizationSwitcher trigger. + * + * - When `true`, renders the active organization's Clerk Billing plan name. + * Free plans use the `primary` color scheme; paid plans use `secondary`. + * - When a function is provided, it is called asynchronously to resolve + * custom badge data: `{ label, slug, colorScheme? }`. + * - When `false` or omitted, no badge is rendered. * * `@default` false */ renderPlanBadge?: boolean | (() => Promise<{ label: string; slug: string; colorScheme?: 'primary' | 'secondary' }>);🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@packages/shared/src/types/clerk.ts` around lines 2229 - 2234, Update the JSDoc for the public renderPlanBadge property to document both supported modes: the boolean behavior for showing the active organization’s billing plan name and the async function option for custom badge data. Describe that the function returns an object containing label and slug, with optional colorScheme limited to primary or secondary; retain the existing default annotation.Source: Path instructions
packages/ui/src/components/OrganizationSwitcher/OrganizationSwitcherTrigger.tsx (1)
129-129: 📐 Maintainability & Code Quality | 🟠 Major | ⚡ Quick win
LoadingBadgeinstances lack element descriptors for theming. Both loading-state badges render withoutelementDescriptororelementId, so appearance overrides cannot target the loading placeholder. The shared root cause is one missing descriptor applied to bothLoadingBadgeusages.
packages/ui/src/components/OrganizationSwitcher/OrganizationSwitcherTrigger.tsx#L129: AddelementDescriptor={descriptors.organizationSwitcherTriggerBadge}andelementId={descriptors.organizationSwitcherTriggerBadge.setId('loading')}to theLoadingBadgeinClerkBillingPlanBadge.packages/ui/src/components/OrganizationSwitcher/OrganizationSwitcherTrigger.tsx#L170: Apply the sameelementDescriptorandelementIdto theLoadingBadgeinCustomRenderPlanBadge.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@packages/ui/src/components/OrganizationSwitcher/OrganizationSwitcherTrigger.tsx` at line 129, Update both LoadingBadge usages in ClerkBillingPlanBadge at packages/ui/src/components/OrganizationSwitcher/OrganizationSwitcherTrigger.tsx#L129 and CustomRenderPlanBadge at packages/ui/src/components/OrganizationSwitcher/OrganizationSwitcherTrigger.tsx#L170 to include descriptors.organizationSwitcherTriggerBadge as elementDescriptor and its setId('loading') result as elementId.Source: Path instructions
🧹 Nitpick comments (2)
packages/shared/src/types/clerk.ts (1)
2234-2234: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick winConsider extracting the inline function type for reuse.
The return type
() => Promise<{ label: string; slug: string; colorScheme?: 'primary' | 'secondary' }>is duplicated verbatim inOrganizationSwitcherTrigger.tsx(line 151). Extracting a named type (e.g.,RenderPlanBadgeResult) would keep the contract in sync if the shape changes.♻️ Proposed type extraction
+export type PlanBadgeData = { + label: string; + slug: string; + colorScheme?: 'primary' | 'secondary'; +}; + renderPlanBadge?: boolean | (() => Promise<PlanBadgeData>);🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@packages/shared/src/types/clerk.ts` at line 2234, Extract the duplicated render-plan-badge callback result shape into a named shared type, such as RenderPlanBadgeResult, in the shared Clerk types. Update the renderPlanBadge declaration and OrganizationSwitcherTrigger.tsx callback type to reference it, preserving the existing fields and optional colorScheme contract.Source: Coding guidelines
packages/ui/src/components/OrganizationSwitcher/OrganizationSwitcherTrigger.tsx (1)
158-167: 🩺 Stability & Availability | 🔵 Trivial | ⚡ Quick winAdd cleanup to
CustomRenderPlanBadge'suseEffect.The async IIFE has no cleanup — if the component unmounts before the promise resolves,
setStatefires on an unmounted component and the async operation continues unnecessarily. Add anisMountedguard.🔒 Proposed fix
useEffect(() => { + let isMounted = true; (async () => { try { const data = await renderPlanBadge(); - setState({ isLoading: false, data }); + if (isMounted) setState({ isLoading: false, data }); } catch { - setState({ isLoading: false, data: null }); + if (isMounted) setState({ isLoading: false, data: null }); } })(); + return () => { isMounted = false; }; }, [renderPlanBadge]);🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@packages/ui/src/components/OrganizationSwitcher/OrganizationSwitcherTrigger.tsx` around lines 158 - 167, Update the useEffect invoking renderPlanBadge in CustomRenderPlanBadge with an isMounted guard: set it true before starting the async operation, check it before both setState calls, and return cleanup that marks it false. Preserve the existing loading, success, and error state values while preventing updates after unmount.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Outside diff comments:
In `@packages/shared/src/types/clerk.ts`:
- Around line 2229-2234: Update the JSDoc for the public renderPlanBadge
property to document both supported modes: the boolean behavior for showing the
active organization’s billing plan name and the async function option for custom
badge data. Describe that the function returns an object containing label and
slug, with optional colorScheme limited to primary or secondary; retain the
existing default annotation.
In
`@packages/ui/src/components/OrganizationSwitcher/OrganizationSwitcherTrigger.tsx`:
- Line 129: Update both LoadingBadge usages in ClerkBillingPlanBadge at
packages/ui/src/components/OrganizationSwitcher/OrganizationSwitcherTrigger.tsx#L129
and CustomRenderPlanBadge at
packages/ui/src/components/OrganizationSwitcher/OrganizationSwitcherTrigger.tsx#L170
to include descriptors.organizationSwitcherTriggerBadge as elementDescriptor and
its setId('loading') result as elementId.
---
Nitpick comments:
In `@packages/shared/src/types/clerk.ts`:
- Line 2234: Extract the duplicated render-plan-badge callback result shape into
a named shared type, such as RenderPlanBadgeResult, in the shared Clerk types.
Update the renderPlanBadge declaration and OrganizationSwitcherTrigger.tsx
callback type to reference it, preserving the existing fields and optional
colorScheme contract.
In
`@packages/ui/src/components/OrganizationSwitcher/OrganizationSwitcherTrigger.tsx`:
- Around line 158-167: Update the useEffect invoking renderPlanBadge in
CustomRenderPlanBadge with an isMounted guard: set it true before starting the
async operation, check it before both setState calls, and return cleanup that
marks it false. Preserve the existing loading, success, and error state values
while preventing updates after unmount.
ℹ️ Review info
⚙️ Run configuration
Configuration used: Repository YAML (base), Repository UI (inherited)
Review profile: CHILL
Plan: Pro Plus
Run ID: 1ad36dae-eb01-4893-81bc-ff999f5a9e6e
📒 Files selected for processing (3)
packages/shared/src/types/clerk.tspackages/ui/src/components/OrganizationSwitcher/OrganizationSwitcherTrigger.tsxpackages/ui/src/components/OrganizationSwitcher/__tests__/OrganizationSwitcher.test.tsx
🚧 Files skipped from review as they are similar to previous changes (1)
- packages/ui/src/components/OrganizationSwitcher/tests/OrganizationSwitcher.test.tsx




Description
This PR adds a new prop to the
OrganizationSwitchercomponent,showPlanName, that renders a badge when the current active organization is subscribed to a Clerk Billing Plan. The badge has an animated loading state (since it's resolved separately from the Organization information). Free plans use theprimarycolor scheme, whereas paid plans use thesecondary. This gives paid plans more visual weight.Checklist
pnpm testruns as expected.pnpm buildruns as expected.Type of change
Summary by CodeRabbit
showPlanName) to display the active organization’s Clerk Billing plan name in the Organization Switcher.